<?php
namespace zake\foundation\reactor\driver;

use zake\foundation\able\reactor\LoopAdaptAble;
use zake\helper\Console;

/**
 * Class SelectAdapt
 * @package zake\foundation\loop
 */
class SelectAdapt implements LoopAdaptAble
{

    private $_readResourceMap   = [];
    private $_writeResourceMap  = [];
    private $_timeScheduler     = null;

    private $_readCallbackMap      = [];
    private $_writeCallbackMap     = [];
    private $_timeCallBackMap      = [];
    private $_signalCallBackMap    = [];



    /**
     * select 超时时间 100s
     * @var int
     */
    private $_timeOut = 100000000;


    /**
     *
     * @var int
     */
    private $_timeId = 0;


    /**
     * 构造器
     * @return SelectAdapt|LoopAdaptAble
     * @author liu.bin 2020-04-03 23:16
     */
    public static function create()
    {
        return new static();
    }


    /**
     *
     * @author liu.bin 2020-03-26 13:23
     */
    public function loop()
    {
        while (true){

            // 注册信号监听
            pcntl_signal_dispatch();

            $readList   = array_values($this->_readResourceMap);
            $writeList  = array_values($this->_writeResourceMap);
            if(empty($readList) && empty($writeList)){
                Console::error("read list and write list is empty. ");
                break;
            }

            //Console::dump([$readList,$writeList],'RW-select');

            // 此处阻塞
            $result = @stream_select($readList,$writeList,$except,0,$this->_timeOut);

            // 定时器调度监听
            $this->tick_dispatch();
            if(!$result){
                Console::error("select timeout. result[{$result}]");
                continue;
            }

            // 读
            foreach ($readList as $resource){
                if(isset($this->_readCallbackMap[(int)$resource])){
                    $this->_readCallbackMap[(int)$resource]();
                }
            }
            // 写
            foreach ($writeList as $resource){
                if(isset($this->_writeCallbackMap[(int)$resource])){
                    $this->_writeCallbackMap[(int)$resource]();
                }
            }
        }

    }


    /**
     *
     * @param resource $resource
     * @return bool
     * @author liu.bin 2020-04-26 09:26
     */
    public function addRead($resource,\Closure $handler)
    {
        $this->_readResourceMap[(int)$resource] = $resource;
        $this->_readCallbackMap[(int)$resource] = $handler;
        return true;
    }


    /**
     *
     * @param $resource
     * @return bool
     *
     * @author liu.bin 2020-04-26 09:27
     */
    public function addWrite($resource,\Closure $handler)
    {
        $fd = fd($resource);
        if($fd){
            $this->_writeResourceMap[$fd] = $resource;
            $this->_writeCallbackMap[$fd] = $handler;
            return true;
        }
        return false;
    }


    /**
     *
     *
     * @author liu.bin 2020-04-27 18:12
     */
    public function addTime($interval,\Closure $handler,$param=[])
    {
        $runTime    = microtime(true) + $interval;
        $timeId     = ++$this->_timeId;
        $this->_timeScheduler->insert($timeId, -$runTime);
        $this->_timeCallBackMap[$timeId] = [$interval,$handler,$param];

        // 重置超时时间
        $timeout = ($runTime - microtime(true)) * 1000000;
        if( $this->_timeOut > $timeout ){
            $this->_timeOut = $timeout;
        }
        return $timeId;
    }


    /**
     * 添加信号处理
     * @param int $sigNo
     * @param \Closure $handler
     *
     * @author liu.bin 2020-04-28 11:38
     */
    public function addSignal($sigNo,\Closure $handler)
    {
        $this->_signalCallBackMap[$sigNo] = $handler;
        pcntl_signal($sigNo, array($this, 'signalHandler'));
    }


    /**
     *
     * @param resource|int $resource
     * @return bool;
     * @author liu.bin 2020-04-15 21:58
     */
    public function removeRead($resource)
    {
        if(isset($this->_readResourceMap[(int)$resource])){
            unset($this->_readResourceMap[(int)$resource],$this->_readCallbackMap[(int)$resource]);
        }
        return true;
    }


    /**
     *
     * @param resource|int $resource
     * @return bool
     *
     * @author liu.bin 2020-04-25 18:21
     */
    public function removeWrite($resource)
    {
        if(isset($this->_writeResourceMap[(int)$resource])){
            unset($this->_writeResourceMap[(int)$resource],$this->_writeCallbackMap[(int)$resource]);
        }
        return true;
    }


    /**
     *
     *
     * @author liu.bin 2020-04-27 22:44
     */
    public function removeTime($timeId)
    {
        if(isset($this->_timeCallBackMap[$timeId])){
            unset($this->_timeCallBackMap[$timeId]);
        }
        return true;
    }


    /**
     * 移除信号处理
     * @param int $sigNo
     *
     * @author liu.bin 2020-04-28 11:47
     */
    public function removeSignal($sigNo)
    {
        pcntl_signal($sigNo, SIG_IGN);
        unset($this->_signalCallBackMap[$sigNo]);
    }


    /**
     * 构造器
     * SelectAdapt constructor.
     */
    private function __construct()
    {
        $this->_timeScheduler = new \SplPriorityQueue();
        $this->_timeScheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
    }


    /**
     * 时间触发
     *
     * @author liu.bin 2020-04-27 22:47
     */
    protected function tick_dispatch()
    {
        while (!$this->_timeScheduler->isEmpty()) {
            $topNode        = $this->_timeScheduler->top();
            $timeId         = $topNode['data'];
            $nextRunTime    = -$topNode['priority'];
            $now            = microtime(true);
            $this->_timeOut = ($nextRunTime - $now) * 1000000;


            if ($this->_timeOut <= 0) {
                // 到时间了,出列
                $this->_timeScheduler->extract();
                if (!isset($this->_timeCallBackMap[$timeId])) {
                    continue;
                }
                list($interval,$callback,$param) = $this->_timeCallBackMap[$timeId];
                $persistent = $param['persistent']??true;
                if($persistent){
                    $nextRunTime = $now + $interval;
                    $this->_timeScheduler->insert($timeId, -$nextRunTime);
                }else{
                    $this->removeTime($timeId);
                }
                $callback($timeId);
                continue;
            }
            return;
        }
        $this->_timeOut = 100000000;
    }




    /**
     * Signal handler.
     * @param int $sigNo
     */
    public function signalHandler($sigNo)
    {
        $handler = $this->_signalCallBackMap[$sigNo];
        $handler($sigNo);
    }
}
